package org.quincy.rock.core.dao;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.quincy.rock.core.dao.annotation.Column;
import org.quincy.rock.core.dao.annotation.IgnoreInsert;
import org.quincy.rock.core.dao.annotation.IgnoreInsertUpdate;
import org.quincy.rock.core.dao.annotation.IgnoreUpdate;
import org.quincy.rock.core.dao.annotation.JoinTable;
import org.quincy.rock.core.dao.annotation.JoinTables;
import org.quincy.rock.core.dao.annotation.PrimaryKey;
import org.quincy.rock.core.dao.annotation.Table;
import org.quincy.rock.core.dao.annotation.Temporary;
import org.quincy.rock.core.dao.sql.FromTable;
import org.quincy.rock.core.dao.sql.JoinOn;
import org.quincy.rock.core.dao.sql.JoinType;
import org.quincy.rock.core.dao.sql.Predicate;
import org.quincy.rock.core.exception.IllegalClassException;
import org.quincy.rock.core.exception.InvokeException;
import org.quincy.rock.core.exception.NoSuchPropertyException;
import org.quincy.rock.core.function.RtBiConsumer;
import org.quincy.rock.core.function.RtConsumer;
import org.quincy.rock.core.util.ReflectUtil;
import org.quincy.rock.core.util.StringUtil;
import org.springframework.util.ReflectionUtils;

/**
 * <b>实体元数据类。</b>
 * <p><b>详细说明：</b></p>
 * <!-- 在此添加详细说明 -->
 * 无。
 * @version 1.0
 * @author mex2000
 * @since 1.0
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class EntityMetadata {
	/**
	 * entityType。
	 */
	private final Class<?> entityType;
	/**
	 * 忽略持久化的注解类。
	 */
	private final Class<? extends Annotation>[] transientClasses;
	/**
	 * 实体表信息及join表关联信息。
	 */
	private final EntityTable entityTable;

	/**
	 * <b>构造方法。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param clazz 实体类型
	 */
	public EntityMetadata(Class<?> entityType) {
		this(entityType, Temporary.class);
	}

	/**
	 * <b>构造方法。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param clazz 实体类型
	 * @param transientClasses 忽略持久化的注解类
	 */
	@SafeVarargs
	public EntityMetadata(Class<?> entityType, Class<? extends Annotation>... transientClasses) {
		this.entityType = entityType;
		this.transientClasses = transientClasses;
		Table table = ReflectUtil.findAnnotation(Table.class, entityType);
		this.entityTable = table == null ? new EntityTable()
				: new EntityTable(table.name(), table.alias(), table.extraColumns(), table.extraExprColumns());
		this.initialize();
	}

	//初始化bean信息
	private void initialize() {
		//join
		JoinTable[] joinTables = null;
		JoinTables jts = ReflectUtil.findAnnotation(JoinTables.class, entityType);
		if (jts != null) {
			joinTables = jts.value();
		} else {
			JoinTable jt = ReflectUtil.findAnnotation(JoinTable.class, entityType);
			if (jt != null)
				joinTables = new JoinTable[] { jt };
		}
		if (ArrayUtils.isNotEmpty(joinTables)) {
			for (JoinTable joinTable : joinTables) {
				JoinOnTable table = new JoinOnTable(joinTable.joinType(), joinTable.name(), joinTable.alias(),
						joinTable.onExpr(), joinTable.extraColumns(), joinTable.extraExprColumns());
				entityTable.addJoinTable(table);
			}
		}
		//field
		BeanInfo beanInfo = null;
		try {
			beanInfo = Introspector.getBeanInfo(entityType);
		} catch (IntrospectionException e) {
			throw new IllegalClassException(entityType.getName(), e);
		}
		PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
		if (pds != null) {
			for (PropertyDescriptor pd : pds) {
				String name = pd.getName();
				if (!name.equals("class")) {
					Field f = ReflectionUtils.findField(entityType, name);
					boolean canPersisted = !(ReflectUtil.hasAnyAnnotation(f, transientClasses)
							|| ReflectUtil.hasAnyAnnotation(pd.getReadMethod(), transientClasses)
							|| ReflectUtil.hasAnyAnnotation(pd.getWriteMethod(), transientClasses));
					entityTable.addPropDescr(new PropDescr(pd, f, canPersisted));
				}
			}
		}
	}

	/**
	 * <b>返回属性名集合。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 属性名集合
	 */
	public Set<String> propertyNames() {
		return entityTable.propertyNames();
	}

	/**
	 * <b>返回可持久化属性名列表。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 可持久化属性名列表
	 */
	public List<String> persistNames() {
		List<String> list = new ArrayList<>();
		persistNames(name -> list.add(name));
		return list;
	}

	/**
	 * <b>迭代可持久化属性名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param callback Consumer
	 */
	public void persistNames(RtConsumer<String> callback) {
		for (String name : propertyNames()) {
			if (canPersisted(name))
				callback.accept(name);
		}
	}

	/**
	 * <b>返回属性个数。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 属性个数
	 */
	public int propertyCount() {
		return entityTable.propertyCount();
	}

	/**
	 * <b>属性是否能读。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性名称
	 * @return 属性是否能读
	 */
	public boolean canRead(String name) {
		return entityTable.pd(name).canRead();
	}

	/**
	 * <b>属性是否能写。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性名称
	 * @return 属性是否能写
	 */
	public boolean canWrite(String name) {
		return entityTable.pd(name).canWrite();
	}

	/**
	 * <b>该属性是否能持久化。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性名称
	 * @return 是否能持久化
	 */
	public boolean canPersisted(String name) {
		return entityTable.pd(name).canPersisted();
	}

	/**
	 * <b>返回属性类型。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性名称
	 * @return 属性类型
	 */
	public Class<?> propertyType(String name) {
		return entityTable.pd(name).type();
	}

	/**
	 * <b>是否是计算列。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性名称
	 * @return 是否是计算列
	 */
	public boolean isCalculateColumn(String name) {
		PropDescr pd = entityTable.pdMap.get(name);
		return pd == null ? false : pd.calculated();
	}

	/**
	 * <b>返回对应的列名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 在主表中的列名。
	 * @param name 属性名称
	 * @return 对应的列名，如果没有设置则返回null
	 */
	public String columnName(String name) {
		PropDescr pd = entityTable.pdMap.get(name);
		return pd == null ? null : pd.columnName();
	}

	/**
	 * <b>返回对应的列名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 如果设置了别名，返回的列名带有别名：u.f_code。
	 * @param name 属性名称
	 * @param nameInterceptor colName拦截器,可以为null
	 * @return 对应的列名
	 */
	public String columnName(String name, NameInterceptor nameInterceptor) {
		//取得在主表中的列名(可能为null或者是计算表达式)		
		String colName = columnName(name);
		if (isCalculateColumn(name))
			return colName;
		else if (colName == null)
			colName = name;
		//tableAlias
		String tableAlias = tableAlias(name);
		if (tableAlias == null)
			tableAlias = tableAlias();
		if (nameInterceptor == null)
			nameInterceptor = NameInterceptor.NOOP;
		return nameInterceptor.applyColumnName(tableAlias, colName).toString();
	}

	/**
	 * <b>返回主键属性名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 主键属性名，如果没有设置则返回空，如果有名为id的字段，则会把id作为主键字段名称
	 */
	public String pKName() {
		if (pkName == null)
			pkName = entityTable.pkName();
		return pkName;
	}

	private String pkName;

	/**
	 * <b>返回表名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 表名，如果没有设置则返回null
	 */
	public String tableName() {
		return entityTable.getTableName();
	}

	/**
	 * <b>返回表别名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 表别名
	 */
	public String tableAlias() {
		return entityTable.getTableAlias();
	}

	/**
	 * <b>返回属性字段性所属表别名。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性字段名
	 * @return 属性字段性所属表别名,如果没有设置则返回null
	 */
	public String tableAlias(String name) {
		return entityTable.pd(name).tableAlias();
	}

	/**
	 * <b>不是Join表字段。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param name 属性字段名
	 * @return 不是Join表字段
	 */
	public boolean notJoinTableField(String name) {
		String tableAlias = tableAlias(name);
		return StringUtils.isBlank(tableAlias) || tableAlias.equals(tableAlias());
	}

	/**
	 * <b>返回实体名称。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 实体名称
	 */
	public String entityName() {
		return entityType.getSimpleName();
	}

	/**
	 * <b>返回实体表信息。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return EntityTable
	 */
	public EntityTable entityTable() {
		if (!entityTable.corrected) {
			entityTable.corrected = true;
			String tableName = tableName();
			entityTable.setTableName(StringUtil.isBlank(tableName) ? entityName() : tableName);
		}
		return entityTable;
	}

	/**
	 * <b>返回实体类型。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @return 实体类型
	 */
	public Class<?> entityType() {
		return entityType;
	}

	/**
	 * <b>设置属性值。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param entity 实体
	 * @param name 属性名称
	 * @param value 属性值
	 */
	public void setPropertyValue(Object entity, String name, Object value) {
		entityTable.pd(name).setPropertyValue(entity, value);
	}

	/**
	 * <b>获得属性值。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param entity 实体
	 * @param name 属性名称
	 * @return 属性值
	 */
	public <T> T getPropertyValue(Object entity, String name) {
		return (T) entityTable.pd(name).getPropertyValue(entity);
	}

	/**
	 * <b>是否是空的属性值。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param entity 实体
	 * @param name 属性名称
	 * @return 是否是空的属性值
	 */
	public boolean isNullPropertyValue(Object entity, String name) {
		return entityTable.pd(name).getPropertyValue(entity) == null;
	}

	/**
	 * <b>返回where子句。</b>
	 * <p><b>详细说明：</b></p>
	 * <!-- 在此添加详细说明 -->
	 * 无。
	 * @param where Predicate,不能为空
	 * @param applyAlias 是否应用别名
	 * @param nameInterceptor 列名拦截器,不能为空
	 * @param valueInterceptor 值拦截器,不能为空
	 * @return where子句
	 */
	public String where(Predicate where, boolean applyAlias, NameInterceptor nameInterceptor,
			ValueInterceptor valueInterceptor) {
		return where.toString((alias, name, isObj, isCol) -> {
			if (StringUtil.isBlank(alias)) {
				if (applyAlias)
					return columnName(name, nameInterceptor);
				else {
					String colName = columnName(name);
					return nameInterceptor.apply(null, colName == null ? name : colName, isObj, isCol);
				}
			} else
				return nameInterceptor.apply(alias, name, isObj, isCol);
		}, valueInterceptor);
	}

	public static class PropDescr {
		private PropertyDescriptor pd;
		private boolean canPersisted;
		private Column column;
		private PrimaryKey primaryKey;

		private boolean ignoreInsert;
		private boolean ignoreUpdate;

		private String colName;

		PropDescr(PropertyDescriptor pd, Field field, boolean canPersisted) {
			this.pd = pd;
			this.canPersisted = canPersisted;
			column = ReflectUtil.findAnnotation(Column.class, field, pd.getReadMethod(), pd.getWriteMethod());
			if (column != null && !StringUtil.isBlank(column.name())) {
				this.colName = column.name();
				if (column.calculated()) {
					ignoreInsert = true;
					ignoreUpdate = true;
				} else if (column.ignoreInsert())
					ignoreInsert = true;
				else if (column.ignoreUpdate())
					ignoreUpdate = true;
			}
			//primaryKey
			primaryKey = ReflectUtil.findAnnotation(PrimaryKey.class, field, pd.getReadMethod(), pd.getWriteMethod());
			if (primaryKey != null && !StringUtil.isBlank(primaryKey.name())) {
				this.colName = primaryKey.name();
			}
			//ignoreInsertUpdate
			if (ReflectUtil.findAnnotation(IgnoreInsertUpdate.class, field, pd.getReadMethod(),
					pd.getWriteMethod()) == null) {
				if (ReflectUtil.findAnnotation(IgnoreInsert.class, field, pd.getReadMethod(),
						pd.getWriteMethod()) != null)
					ignoreInsert = true;
				if (ReflectUtil.findAnnotation(IgnoreUpdate.class, field, pd.getReadMethod(),
						pd.getWriteMethod()) != null)
					ignoreUpdate = true;
			} else {
				ignoreInsert = true;
				ignoreUpdate = true;
			}
		}

		public String name() {
			return pd.getName();
		}

		public boolean canRead() {
			return pd.getReadMethod() != null;
		}

		public boolean canWrite() {
			return pd.getWriteMethod() != null;
		}

		public Class<?> type() {
			return pd.getPropertyType();
		}

		public boolean canPersisted() {
			return canPersisted;
		}

		public boolean ignoreInsert() {
			return ignoreInsert;
		}

		public boolean ignoreUpdate() {
			return ignoreUpdate;
		}

		public String columnName() {
			return colName;
		}

		public boolean primaryKey() {
			return primaryKey != null;
		}

		public String tableAlias() {
			return column == null ? null : column.tableAlias();
		}

		public boolean calculated() {
			return column != null && column.calculated();
		}

		public void setPropertyValue(Object entity, Object value) {
			Method m = pd.getWriteMethod();
			if (m != null) {
				try {
					m.invoke(entity, value);
				} catch (IllegalAccessException | InvocationTargetException e) {
					throw new InvokeException(e.getMessage(), e);
				}
			}
		}

		public Object getPropertyValue(Object entity) {
			Method m = pd.getReadMethod();
			try {
				return m.invoke(entity);
			} catch (IllegalAccessException | InvocationTargetException e) {
				throw new InvokeException(e.getMessage(), e);
			}
		}
	}

	public static class JoinOnTable extends JoinOn {

		/**
		 * serialVersionUID。
		 */
		private static final long serialVersionUID = 1L;

		private String[] extraColumns;

		private String[] extraExprColumns;

		JoinOnTable(JoinType joinType, String tableName, String tableAlias, String onExpr, String[] extraColumns,
				String[] extraExprColumns) {
			super(joinType, tableName, tableAlias, onExpr);
			this.extraColumns = extraColumns;
			this.extraExprColumns = extraExprColumns;
		}

		public String[] getExtraColumns() {
			return extraColumns;
		}

		public String[] getExtraExprColumns() {
			return extraExprColumns;
		}

		public JoinOnTable extraColumn(RtBiConsumer<String, String> consumer) {
			if (ArrayUtils.isNotEmpty(extraColumns)) {
				for (String colName : extraColumns) {
					consumer.accept(getTableAlias(), colName);
				}
			}
			return this;
		}

		public JoinOnTable extraExprColumn(RtBiConsumer<String, String> consumer) {
			if (ArrayUtils.isNotEmpty(extraExprColumns)) {
				for (String expr : extraExprColumns) {
					consumer.accept(getTableAlias(), expr);
				}
			}
			return this;
		}
	}

	public static class EntityTable extends FromTable {

		/**
		 * serialVersionUID。
		 */
		private static final long serialVersionUID = 1L;

		private String[] extraColumns;

		private String[] extraExprColumns;

		private final Map<String, PropDescr> pdMap = new TreeMap<>();

		private boolean corrected;

		EntityTable() {
		}

		EntityTable(String tableName, String tableAlias, String[] extraColumns, String[] extraExprColumns) {
			super(tableName, tableAlias);
			this.extraColumns = extraColumns;
			this.extraExprColumns = extraExprColumns;
		}

		public String[] getExtraColumns() {
			return extraColumns;
		}

		public String[] getExtraExprColumns() {
			return extraExprColumns;
		}

		public List<JoinOnTable> getJoinOnTables() {
			return (List) super.getJoinTables();
		}

		public Set<String> propertyNames() {
			return pdMap.keySet();
		}

		public int propertyCount() {
			return pdMap.size();
		}

		public PropDescr pd(String name) {
			PropDescr pd = pdMap.get(name);
			if (pd == null)
				throw new NoSuchPropertyException(name);
			return pd;
		}

		public String pkName() {
			for (PropDescr pd : pdMap.values()) {
				if (pd.primaryKey != null)
					return pd.name();
			}
			return pdMap.containsKey("id") ? "id" : null;
		}

		private void addPropDescr(PropDescr pd) {
			pdMap.put(pd.name(), pd);
		}

		public EntityTable extraColumn(RtBiConsumer<String, String> consumer) {
			if (ArrayUtils.isNotEmpty(extraColumns)) {
				for (String colName : extraColumns) {
					consumer.accept(getTableAlias(), colName);
				}
			}
			if (hasJoin()) {
				for (JoinOnTable table : getJoinOnTables()) {
					table.extraColumn(consumer);
				}
			}
			return this;
		}

		public EntityTable extraExprColumn(RtBiConsumer<String, String> consumer) {
			if (ArrayUtils.isNotEmpty(extraExprColumns)) {
				for (String colName : extraExprColumns) {
					consumer.accept(getTableAlias(), colName);
				}
			}
			if (hasJoin()) {
				for (JoinOnTable table : getJoinOnTables()) {
					table.extraExprColumn(consumer);
				}
			}
			return this;
		}
	}
}
